<?php

/**
 * alltosun.com 路由规则类 Rule.php
 * ============================================================================
 * 版权所有 (C) 2007-2011 北京互动阳光科技有限公司，并保留所有权利。
 * 网站地址: http://www.alltosun.com
 * ----------------------------------------------------------------------------
 * 许可声明：这是一个开源程序，未经许可不得将本软件的整体或任何部分用于商业用途及再发布。
 * ============================================================================
 * $Author: 高竞竞 (gaojj@alltosun.com) $
 * $Date: 2010-12-31 下午03:27:11 $
 * $Id: Rule.php 27924 2013-11-28 03:19:58Z qianym $
*/

class AnRule
{
    /**
     * AnRule实例
     * @var obj
     */
    private static $instance;

    /**
     * 待解析的自定义规则
     * @var array
     */
    private $rules = array();

    /**
     * 待解析的别名规则
     * @var array
     */
    private $alias = array();

    /**
     * 待解析的跳转规则
     * @var array
     * @todo 待实现跳转规则的解析
     */
    private $redirect = array();

    /**
     * 解析出来的控制器子目录名数组
     * @var array
     */
    private $dirs = array();

    /**
     * 解析出来的变量数组
     * @var array
     */
    private $vars = array();

    /**
     * 解析出来的附加参数数组
     * @var array
     */
    private $params = array();

    /**
     * 开始解析参数部分
     * @var bool
     */
    private $paramsParsing = false;

    /**
     * 解析匹配成功
     * @var bool
     */
    private $parseMatched = false;

    /**
     * 是否已采用默认规则进行解析
     * @var bool
     */
    private $defaultRuleParsed = false;

    /**
     * 本次匹配符合的规则索引，对应$this->rules数组
     * @var int
     */
    private $matchedRule = null;

    /**
     * 是否开启debug模式（默认关闭）
     * @var bool
     */
    private $debug = false;

    /**
     * 开始解析的时间
     * @var float
     */
    private $start_time = null;

    /**
     * 控制器基准目录
     * @var string
     */
    private $controllerDir = '';

    private function __construct(){}
    private function __clone(){}

    /**
     * 单例方法
     * @return obj
     */
    public static function getInstance()
    {
        if (empty(self::$instance)) {
           self::$instance = new self();
        }
        return self::$instance;
    }

    /*
    public function __call($fun, $params = array())
    {
        if (strncasecmp('set', $fun, 3) == 0) {
            $propertyName = strtolower(substr($fun, 3));
            empty($propertyName) && $propertyName = 'rules';
            array_push($this->$propertyName, $params[0]);
            return $this;
        }
        if (strncasecmp('get', $fun, 3) == 0) {
            $propertyName = strtolower(substr($fun, 3));
            empty($propertyName) && $propertyName = 'rules';
            return $this->$propertyName;
        }
    }
    */

    public function __get($name)
    {
        if (isset($this->$name)) {
            return $this->$name;
        }
    }

    /**
     * 设置自定义规则
     * @param string $rule
     * @return object $this
     * @example $rule->set('{$module=blog}/{$app=u}/{$visit_id=\d+}/*');
     * @example $rule->set('{$module=blog}/{$app=u}/{$visit_id|get_user_id_by_url}/*');
     * @example $rule->set('{$module=blog}/{$app=u}/*');
     * @example $rule->set('{$module=blog}/{$app=group}/{$group_id=\d+}/*');
     * @example $rule->set('{$module=blog|admin}/*');
     * @todo $rule->set('http://{$subdomain=blog|space}.*.com/{$module|get_city_list=\w}/{$app=u}/{$visit_id=\d+}/*');
     */
    public function set($rule)
    {
        $this->rules[] = $rule;
        return $this;
    }

    /**
     * 设置别名规则
     * @param $rule 别名规则
     * @return object $this
     * @example $rule->setAlias(array('culture'=>'article')); 默认为controller的替换
     * @todo $rule->setAlias(array('culture/*'=>'article/*'));
     * @todo $rule->setAlias(array('blog/{$customer_url}/*'=>'blog/u/{$customer_url}/*'));
     */
    public function setAlias($rule)
    {
        $this->alias = array_merge($this->alias, $rule);
        return $this;
    }

    public function setDebug($on = true)
    {
        $this->debug = $on;
    }

    /**
     * 是否存在规则变量定义
     * @param string $ruleMeta 规则元数据
     * @return bool
     */
    private function variableDefined($ruleMeta)
    {
        // 2011-7-21 支持类profile/{$visit_id=\d+}/*这样的先匹配指定字符串，再匹配变量的规则
//        return strncasecmp($ruleMeta, '{$', 2) == 0 && !$this->paramsParsing;
        return strncasecmp($ruleMeta, '{$', 2) == 0;
    }

    /**
     * 默认规则解析，优先目录，次文件
     * @param mixed $urlParam url参数（$ruleMeta为*时），或者参数数组（$ruleMeta为指定字符串时）
     * @param string $ruleMeta 规则元数据，为*匹配所有，也为指定字符串
     * @return bool
     */
    private function defaultRuleParse($urlParams, $ruleMeta = '*')
    {
        $debug = $this->debug;
        if ($debug) $this->log("Default rule parse start! ruleMeta:{$ruleMeta} and urlParams:".var_export($urlParams, true), LOG_NOTICE);

        if (empty($urlParams)) return false;
        if ($ruleMeta != '*' && $ruleMeta != $urlParams) {
            // 指定字符串的匹配，比如blog/*中的blog
            if ($debug) $this->log('Match Failed! urlParams is not matching ruleMeta');
            return false;
        }

        $urlParams     = (array)$urlParams;
        $paramsParsing = &$this->paramsParsing;
        $dirs          = &$this->dirs;
        $params        = &$this->params;
        $controllerDir = &$this->controllerDir;

        // 将剩余的url进行目录和参数的划分
        foreach ($urlParams as $v) {
            // 如果已经开始解析参数的话，则不再匹配目录
            if (!$paramsParsing) {
                // 目录
                $dir = !empty($dirs) ? implode('/', $dirs).'/' : '';

                $t_dir = $controllerDir.'/'.$dir.$v;

                if ($debug) $this->log("Parse dir: $t_dir");

                if (is_dir($t_dir)) {
                    // Anti-%00 hack when not using url rewrite
                    if (realpath($t_dir) != $t_dir) {
                        exit('Access Denied');
                    }
                    $dirs[] = $v;

                    if ($debug) $this->log("Dir matched! Continue to next params...");

                    continue;
                }

                if ($debug) $this->log('Dir dismatched! All left urlParams go to $params.');
            }

            $paramsParsing = true;
            $params[] = $v;
        }

        if ($debug) $this->log('Default rule parse end! $params:'.var_export($params, true));

        return true;
    }

    /**
     * 自定义规则解析
     * @param string $urlParam 当前解析规则中的一个url参数
     * @param string $ruleMeta 当前解析规则中匹配的一个规则
     * @return mixed 返回解析后的数组array('varName'=>'varValue')，或者解析不符合返回false
     * @tutorial 支持规则{$varName}，即无规则限制，直接匹配变量
     * @tutorial 支持规则{$varName=pregExpr}，正则规则限制
     * @tutorial 支持规则{$varName|funcName=pregExpr}，数据源，默认数据源函数只传入一个参数，即$urlParam
     * @tutorial 支持规则{$varName|funcName}，纯数据源，无规则限制
     * @todo 支持规则{$varName|funcName:param1:param2=pregExpr}，多个参数的数据源函数，第一个参数为$urlParam
     */
    private function customRuleParse($urlParam, $ruleMeta)
    {
        $debug = $this->debug;
        if ($debug) $this->log("Custom rule parse start! ruleMeta:{$ruleMeta} and urlParam:{$urlParam}");

        $varRuleArr = explode('=', $ruleMeta);

        $varName = substr($varRuleArr[0], 2);

        if (empty($varRuleArr[1])) {
            // 纯变量赋值，无规则解析，如{$varName}或{$varName|funcName}
            $varName = substr($varName, 0, -1);
            if ($debug) $this->log('Pure var define with no pattern, such as {$varName}, {$varName|funcName}');
        }

        if (isset($this->$varName)) {
            throw new AnRuleException("{$varName}已经被设为url内置属性名，不能用于自定义属性名");
        }

        if (!empty($varRuleArr[1])) {
            // 规则匹配
            $rule = substr($varRuleArr[1], 0, -1);
            if ($debug) $this->log("Preg pattern to be matched: {$rule}");
            if (!preg_match("/^{$rule}$/i", $urlParam)) return false;
        }

        if (stripos($varName, '|') !== false) {
            // 数据源匹配
            list($varName, $funcName) = explode('|', $varName);
            // @todo 支持规则{$varName|funcName:param1:param2=pregExpr}，多个参数的数据源函数，第一个参数为$urlParam
            /*
            $funcParams = explode(':', $funcName);
            $funcName   = array_shift($funcParams);
            array_unshift($funcParams, $urlParam);
            */
            $funcParams = $urlParam;
            $varValue = call_user_func_array($funcName, array($funcParams));
            if ($debug) $this->log("Data source matched.Func:{$funcName};Params:{$funcParams};Result:{$varValue}");
            if (empty($varValue)) return false;
        }

        // 如果没有进行数据源匹配或者匹配结果为true的话
        if (!isset($varValue) || $varValue) {
            $varValue = $urlParam;
        }

        !isset($varValue) && $varValue = $urlParam;

        // 御用模块变量名，模块需要重新修正controller_dir
        if ($varName == 'module') {
            $this->controllerDir = MODULE_PATH."/$varValue/controller";
            if ($debug) $this->log("Parsed with system defined var 'module', changed controllerDir:".$this->controllerDir);
        }

        if ($debug) $this->log("Custom rule parse end! varName:{$varName} and varValue:{$varValue}");

        return array($varName=>$varValue);
    }

    /**
     * 解析规则开始，重置所有参数
     */
    private function parseStart()
    {
        $this->dirs = $this->vars = $this->params = array();
        $this->controllerDir = Config::get('controller_dir');
        // 进入$params解析模式，所有剩余的url参数都进入$params，不再匹配
        $this->paramsParsing = $this->parseMatched = false;
    }

    /**
     * 规则解析
     * @param $url 当前访问的url
     * @return true|null|int 返回解析匹配中的规则索引
     */
    public function parse($url)
    {
        $debug = $this->debug;
        $rules = $this->rules;
        $dirs  = &$this->dirs;
        $vars  = &$this->vars;
        $controllerDir = &$this->controllerDir;

        $qiandebug = Request::getParam('qiandebug', 0);
        if ($qiandebug) {
            var_dump($url, '22');
        }
        if (!$url) {
            // 访问首页
            return true;
        }

        if ($debug) {
            if ($this->start_time === null) $this->start_time = microtime(true);
            $this->log('Rule parsing start!');
        }

        //var_dump($url);
        // @todo 目前的规则要求url中用来传递的参数不能含有小数点
        // @ da 弹出domain START
        /*
        $url_arr = explode('/', $url);
        $company_domain = array_shift($url_arr);    //公司域名存哪？
        $url = implode('/', $url_arr);
        */
        // @ da 弹出domain END

        //list($url) = explode('.', $url, 2);
        $urls      = explode('/', $url);
        //@TODO 验证要做进一步的处理
        if (count($urls) > 1 || (count($urls) == 1 && strpos($urls[0], '.html') !== false )) {
            list($tmp_url) = explode('.', $urls[count($urls)-1], 2);
            $urls[count($urls)-1] = $tmp_url;
        }
        $qiandebug = Request::getParam('qiandebug', 0);
        if ($qiandebug) {
            var_dump($qiandebug, $url, $urls);
        }


        // 上次解析时匹配符合的规则，本次解析从此开始
        $matchedRule = $this->matchedRule;
        if ($matchedRule !== null) {
            $rules = array_slice($rules, $matchedRule + 1);
            if ($debug) $this->log('It seems that last rule does not match any controller files.');
            if ($debug) $this->log("We'll go on parse left rules:".var_export($rules));
        }

        if (!$rules && $this->defaultRuleParsed) {
            // 自定义规则均已匹配完毕，且也采用默认规则匹配过
            if ($debug) $this->log('No more rules! And already parsed with default rule.');
            return null;
        }

        foreach ($rules as $k=>$rule) {
            if ($debug) $this->log("We are working on Rule{$k}: {$rule}......");
            $this->parseStart();

            $ruleMetas = explode('/', $rule);
            $ruleMetasCount = count($ruleMetas);
            foreach ($ruleMetas as $k2=>$ruleMeta) {
                if ($debug) $this->log("Rule meta{$k2}:{$ruleMeta}");
                // url比规则少，不匹配，退出当前规则
                // blog/u/
                // blog/u/7/photo/album/*
                if (empty($urls[$k2]) && $ruleMeta != '*') {
                    if ($debug) $this->log('This meta matches no more url params, break!');
                    break;
                }

                if ($ruleMeta == '*') {
                    //var_dump('当前匹配的rule '.$rule);
                    // 星号为最后一个规则，将剩余的url参数按默认规则解析
                    $urlParams = array_slice($urls, $k2);
                    $this->defaultRuleParse($urlParams);

                    if ($debug) $this->log('Congratulations! This rule matched!');
                    if ($debug) $this->log('Last meta * parsed all the left params:'.var_export($urlParams, true));

                    // 匹配成功，退出所有的规则解析
                    $this->parseMatched = true;
                    break 2;
                }

                $urlParam = $urls[$k2];

                // 解析规则{$var=rule}，{$var|source=\d}
                if ($this->variableDefined($ruleMeta)) {
                    $parsedData = $this->customRuleParse($urlParam, $ruleMeta);

                    if (!$parsedData) break;

                    $vars = array_merge($vars, $parsedData);

                    // @todo module与www的转发，优先www，其次module
                    /*
                    if (!empty($vars['module'])) {
                        $this->defaultRuleParse($urlParam, $ruleMeta);
                        $this->parseMatched();
                    }
                    */

                    // 御用变量名site，用来匹配控制器子目录
                    // eg. {$module}/{$site=blog|admin}/add, {$site=blog}/{$app=u}/*
                    if (isset($parsedData['site'])) {
                        $dir = !empty($dirs) ? implode('/', $dirs).'/' : '';
                        if (is_dir($controllerDir.'/'.$dir.$urlParam)) {
                            $dirs[] = $urlParam;
                            $site_key = count($dirs) - 1;
                        }
                    }
                    // @FIXME 永安临时支持放在site里的module，当site在module规则前时，不再匹配site子目录
                    // eg. {$site=blog}/{$app=u}/{$module=video}/add
                    if (isset($parsedData['module']) && isset($site_key)) {
                        unset($dirs[$site_key]);
                        $dirs = array_values($dirs);
                    }

                } elseif (!$this->defaultRuleParse($urlParam, $ruleMeta)) {
                    break;
                }

                if ($k2 + 1 == $ruleMetasCount) {
                    // 最后一个规则不为*，且正好完全匹配
                    if ($debug) $this->log('Congratulations! This rule matched exactly!');

                    $this->parseMatched = true;
                    //var_dump('当前匹配的rule1 '.$rule);
                    break 2;
                }
            }
        }
        if (!$this->parseMatched) {
            // 没有匹配自定义规则，走默认规则
            $this->defaultRuleParse($urls);
            $this->defaultRuleParsed = true;
            $this->parseMatched = true;

            if ($debug) $this->log('No rule matched. Parsed with default rule.');
        }

        $this->matchedRule = $k;

        $this->parseMatched();

        return $k;
    }

    /**
     * 匹配成功后的数据处理
     */
    private function parseMatched()
    {
        $debug  = $this->debug;
        $dirs   = &$this->dirs;
        $vars   = &$this->vars;
        $params = &$this->params;
        $alias  = &$this->alias;
        $url    = AnUrl::getInstance();

        $controller = array_shift($params);
        $action     = array_shift($params);
        if ($action === NULL) {
            $action = 'index';
        }

        if ($debug) $this->log("Controller:{$controller};Action:{$action}");

        // alias
        // @FIXME 这样的alias处理只支持controller的alias，对于有module或其他自定义的字段则不能实现alias替换
        if (!empty($controller) && in_array($controller, array_keys($alias))) {
            $controllerFile = $alias[$controller];
            if ($debug) $this->log("Alias found:{$controller}->{$controllerFile}");
        } else {
            $controllerFile = $controller;
        }

        $url['dirs'] = $dirs;
        $url['controller'] = $controller;
        $url['controllerFile'] = $controllerFile;
        $url['action'] = $action;
        $url['params'] = $params;

        foreach ($vars as $k=>$v) {
            if (in_array($k, array('controller', 'action', 'controllerFile'))) {
               throw new AnRuleException("{$k}已经被设为url内置属性名，不能用于自定义属性名");
            }
            $url[$k] = $v;
        }

        if ($debug) $this->log('Done! Time: '.(microtime(true)-$this->start_time).'. Result: <br>'.var_export($url, true));
    }

    private function log($message)
    {
        echo $message.'<br />';
    }
}

/**
 * AnRule Exception
 */
class AnRuleException extends Exception {}
?>